package com.zjdiepu.www.net;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * http请求客户端
 */
public class HttpClient {
	private static final Logger logger = LoggerFactory.getLogger(HttpClient.class);
	
    private static String certPathname;
    private static String certPassword;
    private static CloseableHttpClient httpClient = null;
    private static IdleConnectionMonitorThread idleConnectionMonitorThread = null;
    private static int connectMaxTotal = 200;// 连接池最大连接数
    private static int connectMaxDefault = 20;// 连接池每个连接最大数
    private static int connectRecyclingTime = 5;// 连接回收检测时间(秒)
    private static int connectFreeTimeout = 30;// 连接空闲回收时间(秒)
    private static int connectKeepAliveDuration = 30;// 连接维持时间(秒)
    private static int connectRetryNumber = 5;
    private static String proxyUri;//代理ip
    private static int proxyPort;//代理端口
    private static String proxyScheme;//代理协议
    private static String proxyUserName;//代理服务器用户名
    private static String proxyPassword;//代理服务器密码
    private static Charset charset = Consts.UTF_8;

    //每个请求的超时时间:简单数据
    private static int connectTimeout = 15000;//默认连接超时时间 //请求超时,连接超时时间(单位毫秒) 
    private static int connectRequestTimeout = 15000;//默认请求超时时间
    private static int socketTimeout = 30000;//默认socket超时时间 //响应超时,读数据超时时间(单位毫秒)
    
    //每个请求的超时时间:文件传输
    private static int fileTransferConnectTimeout = 3000;//默认连接超时时间 //请求超时,连接超时时间(单位毫秒) 
    private static int fileTransferConnectRequestTimeout = 3000;//默认请求超时时间
    private static int fileTransferSocketTimeout = 30000;//默认socket超时时间 //响应超时,读数据超时时间(单位毫秒)

    public HttpClient() {
    }

    /**
     * httpGet请求
     * @param uri      uri地址
     * @param paramMap 请求参数
     * @return http返回值
     */
    public static String get(String uri, Map<String, String> paramMap) {
        return get(uri, paramMap, null);
    }

    /**
     * httpGet请求
     *
     * @param uri       uri地址
     * @param paramMap  请求参数
     * @param headerMap http请求头
     * @return http返回值
     */
    public static String get(String uri, Map<String, String> paramMap, Map<String, String> headerMap){
        if (paramMap != null) {
            uri = uri + "?" + StringUtils.join(paramMap.entrySet().stream().
                    map(p -> p.getKey() + "=" + p.getValue()).collect(Collectors.toList()), '&');
        }
        return getCall(uri, headerMap, false);
    }

    /**
     * postFrom请求
     * @param uri      uri地址
     * @param paramMap 请求参数
     * @return http返回值
     */
    public static String postForm(String uri, Map<String, String> paramMap) {
        return postForm(uri, paramMap, null);
    }

    /**
     * postFrom请求
     *
     * @param uri       uri地址
     * @param paramMap  请求参数
     * @param headerMap http请求头
     * @return http返回值
     */
    public static String postForm(String uri, Map<String, String> paramMap, Map<String, String> headerMap) {
           return postFormCall(uri, paramMap, headerMap);
    }

    /**
     * postJson请求
     * @param uri         uri地址
     * @param requestJson 请求参数
     * @return http返回值
     */
    public static String postJson(String uri, String requestJson) {
        return postJson(uri, requestJson, null);
    }


    /**
     * postJson请求
     * @param uri         uri地址
     * @param requestJson 请求参数
     * @param headerMap   http请求头
     * @return http返回值
     */
    public static String postJson(String uri, String requestJson, Map<String, String> headerMap) {
        return postJsonCall(uri, false, requestJson, headerMap);
    }

    /**
     * 请书的postJson请求
     *
     * @param uri         uri地址
     * @param requestJson 请求参数
     * @return http返回值
     */
    public static String postJsonUseCert(String uri, String requestJson) {
        return postJsonUseCert(uri, requestJson, null);
    }

    /**
     * 请书的postJson请求
     *
     * @param uri         uri地址
     * @param requestJson 请求参数
     * @param headerMap   http请求头
     * @return http返回值
     */
    public static String postJsonUseCert(String uri, String requestJson, Map<String, String> headerMap) {
        return postJsonCall(uri, true, requestJson, headerMap);
    }

    /**
     * 客户端关闭
     */
    public synchronized static void close() {
            if (httpClient != null)
				try {
					httpClient.close();
				} catch (IOException e) {
					logger.error("客户端关闭(回收).异常栈:{}", e);
				}
            if (idleConnectionMonitorThread != null)
                idleConnectionMonitorThread.shutdown();
    }

    /**
     * 发送post表单请求
     * @param uri                   uri地址
     * @param map                   请求参数
     * @param headerMap             http请求头
     * @return http返回值
     */
    private static String postFormCall(String uri, Map<String, String> map, Map<String, String> headerMap){
    	String url = getStandardUri(uri);
    	if(url == null) {
    		return null;
    	}
        HttpPost httpPost = new HttpPost(url);
        List<NameValuePair> formParams = map.entrySet().stream().
                map(p -> new BasicNameValuePair(p.getKey(), p.getValue())).collect(Collectors.toList());
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, charset);
        httpPost.setEntity(entity);
        return httpCall(httpPost, httpClient, headerMap, false);
    }
    
    /**
     * @param uri     http请求地址
     * @param isUseCert   是否使用证书
     * @param requestJson 请求内容
     * @return 返回值
     */
    private static String postJsonCall(String uri, boolean isUseCert, String requestJson, Map<String, String> headerMap) {
        return postCall(uri, requestJson, "application/json", isUseCert, headerMap);
    }

    /**
     * 以json格式发送post请求
     *
     * @param uri                   uri地址
     * @param requestContent        请求参数
     * @param ctString              Content-Type
     * @param isUseCert             是否使用证书
     * @param headerMap             http请求头
     * @return http返回值
     */
    private static String postCall(String uri, String requestContent, String ctString, boolean isUseCert, Map<String, String> headerMap){
        CloseableHttpClient httpclient;
        if (isUseCert) {
            httpclient = getSSLHttpClient();
        } else {
            httpclient = httpClient;
        }
        if(null == httpclient) {
        	return null;
        }
        HttpPost httpPost = new HttpPost(getStandardUri(uri));
        StringEntity stringEntity = new StringEntity(requestContent, charset);
        stringEntity.setContentType(ctString);
        httpPost.setEntity(stringEntity);
        return httpCall(httpPost, httpclient, headerMap, isUseCert);
        
    }

    /**
     * 发送get请求
     * @param uri                   uri地址
     * @param headerMap             http请求头
     * @param isUseCert
     * @return http返回值
     */
    private static String getCall(String uri, Map<String, String> headerMap, boolean isUseCert){
        HttpGet httpGet = new HttpGet(getStandardUri(uri));
        return httpCall(httpGet, httpClient, headerMap, isUseCert);
    }

    /**
     * @param request    httpRequest
     * @param httpClient
     * @param headerMap  http请求头
     * @param isUseCert
     * @return http返回值
     */
    private static String httpCall(HttpRequestBase request, CloseableHttpClient httpClient, Map<String, String> headerMap, boolean isUseCert) {
    	RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectTimeout)
                .setConnectionRequestTimeout(connectRequestTimeout)
                .setSocketTimeout(socketTimeout).build();
    	request.setConfig(requestConfig);
    	
    	Header[] headers = getHeaders(headerMap);
        if (headers.length != 0)
            request.setHeaders(headers);
        try (CloseableHttpResponse response = httpClient.execute(request)) {
            if (response.getStatusLine().getStatusCode() != 200) {
            	logger.debug("请求响应:业务处理失败.状态码:{},响应结果:{}", response.getStatusLine().getStatusCode(), EntityUtils.toString(response.getEntity()));
            	return null;
            }
			return EntityUtils.toString(response.getEntity(), charset);
        } catch (ParseException | IOException e) {
        	logger.error("请求响应:业务处理成功:结果解析失败.异常栈:{}", e);
		} finally {
            if (isUseCert){
            	try {
            		httpClient.close();
				} catch (IOException e) {
					logger.error("SSL客户端关闭(回收).异常栈:{}", e);
				}
            }
        }
        return null;
    }

    /**
     * @return 返回ssl httpClient(没有连接池)
     */
    private static CloseableHttpClient getSSLHttpClient() {
    	try (FileInputStream inStream = new FileInputStream(new File(certPathname))) {
    		KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(inStream, certPassword.toCharArray());
            SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, certPassword.toCharArray()).build();
            // 指定TLS版本
            SSLConnectionSocketFactory sslSf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null,
            		SSLConnectionSocketFactory.getDefaultHostnameVerifier());
            // 设置httpclient的SSLSocketFactory
            return HttpClients.custom().setSSLSocketFactory(sslSf).build();
        } catch (Exception e) {
        	logger.error("创建SSL客户端.异常栈:{}", e);
		}
    	return null;
    }

    /**
     * 请求重试HANDLER
     * @return HttpRequestRetryHandler
     */
    private static HttpRequestRetryHandler getHttpRequestRetryHandler() {
        return (IOException exception, int executionCount, HttpContext context) -> {
            if (executionCount >= connectRetryNumber /*** 超过重试次数 ***/) {
                // Do not retry if over max retry count
                return false;
            }
            
            if(exception instanceof SocketTimeoutException /*** socket传输超时时出现的错误 ***/){
            	//Socket Timeout often already processed
            	return false;
            }
            
            if (exception instanceof InterruptedIOException /*** IO中断时抛出的异常 ***/) {
                // Timeout
                return false;
            }
            if (exception instanceof UnknownHostException /*** 域名解析失败 ***/) {
                // Unknown host
                return false;
            }
            if (exception instanceof SSLException /*** SSL处理失败 ***/) {
                // SSL handshake exception
                return false;
            }
            
            HttpClientContext clientContext = HttpClientContext.adapt(context);
            org.apache.http.HttpRequest request = clientContext.getRequest();
            return !(request instanceof HttpEntityEnclosingRequest);
        };
    }

    /**
     * 连接池参数设置
     * @return PoolingHttpClientConnectionManager
     */
    private static PoolingHttpClientConnectionManager getPoolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        // Increase max total connection to 200
        cm.setMaxTotal(connectMaxTotal);
        // Increase default max connection per route to 20
        cm.setDefaultMaxPerRoute(connectMaxDefault);
        return cm;
    }


    /**
     * 连接存活策略
     */
    private static ConnectionKeepAliveStrategy getConnectionKeepAliveStrategy(){
        return (HttpResponse response, HttpContext context) -> {
            // Honor 'keep-alive' header
            HeaderElementIterator it = new BasicHeaderElementIterator(
                    response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && param.equalsIgnoreCase("timeout")) {
                    return Long.parseLong(value) * 1000;
                }
            }
            return connectKeepAliveDuration * 1000;
        };
    }

    /**
     * 连接回收策略
     */
    public static class IdleConnectionMonitorThread extends Thread {
        private final HttpClientConnectionManager connMgr;
        private volatile boolean shutdown;
        private int connectRecyclingTime;
        private int connectFreeTimeout;

        public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr,
                                           int connectRecyclingTime,
                                           int connectFreeTimeout) {
            super();
            this.connectRecyclingTime = connectRecyclingTime * 1000;
            this.connectFreeTimeout = connectFreeTimeout;
            shutdown = false;
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(connectRecyclingTime);
                        // Close expired connections
                        connMgr.closeExpiredConnections();
                        // Optionally, close connections
                        // that have been idle longer than 30 sec
                        connMgr.closeIdleConnections(connectFreeTimeout, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                // terminate
            	logger.error("idle connections monitor thread happened a exception.{}",ex);
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }

    }
    
    /**
     * 下载文件
     * @param uri      uri地址
     * @param filePath 文件路径
     * @return http返回流
     * @throws SocketTimeoutException 
     * @throws HttpClientException
     */
    public static boolean downloadFile(String uri, String filePath) {
        return downloadFile(uri, filePath, null);
    }

    /**
     * 下载文件
     *
     * @param uri                   uri地址
     * @param filePath              文件路径
     * @param headerMap             http请求头
     * @return http返回流
     * @throws SocketTimeoutException 
     * @throws HttpClientException
     */
    public static boolean downloadFile(String uri, String filePath, Map<String, String> headerMap) {
        if (!createFolder(filePath))
            return false;
        HttpGet httpGet = new HttpGet(uri);
        Header[] headers = getHeaders(headerMap);
        if (headers.length != 0)
            httpGet.setHeaders(headers);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(fileTransferConnectTimeout)
                .setConnectionRequestTimeout(fileTransferConnectRequestTimeout)
                .setSocketTimeout(fileTransferSocketTimeout).build();
        httpGet.setConfig(requestConfig);
        try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
        	Header lenHeader = response.getFirstHeader("Content-Length");
        	if(lenHeader != null && "0".equals(lenHeader.getValue())){
        		return false;
        	}
            InputStream stream = response.getEntity().getContent();
            FileUtils.copyInputStreamToFile(stream, new File(filePath));
            return true;
        } catch (IOException e) {
        	logger.error("下载文件失败,uri:{},filePath:{}.异常栈：{}", uri, filePath, e);
		}
        return false;
    }

    /**
     * 创建文件夹
     * @param filePath 文件路径
     * @return 是否成功
     */
    private static boolean createFolder(String filePath) {
        File file = new File(filePath);
        File fileFolder = new File(file.getParentFile().getAbsolutePath());
        if (!fileFolder.exists()) {
            return fileFolder.mkdirs();
        }
        return true;
    }

    /**
     * 返回标准的uri
     *
     * @param strUrl
     * @return
     */
    private static String getStandardUri(String strUrl) {
        try {
            URL url = new URL(strUrl);
            return new URI(url.getProtocol(), null, url.getHost(), url.getPort(), url.getPath(), url.getQuery(), null).toString();
		} catch (URISyntaxException | MalformedURLException e) {
			logger.error("创建标准Uri失败,strUrl:{}.异常栈:{}",strUrl, e);
		}
        return null;
    }

    /**
     * 返回http请求头列表
     *
     * @param headerMap
     * @return
     */
    private static Header[] getHeaders(Map<String, String> headerMap) {
        if (MapUtils.isEmpty(headerMap))
            return new Header[0];
        Header[] headers = new Header[headerMap.size()];
        int i = 0;
        for (Map.Entry<String, String> entry : headerMap.entrySet()) {
            headers[i++] = new BasicHeader(entry.getKey(), entry.getValue());
        }
        return headers;
    }
	
	/**
	 * @author xiongxiaotun <br/>
	 * @Title: destory  <br/>
	 * @Description: 销毁方法的注解方式  <br/>
	 * @version V1.0  <br/>
	 */
	public void destory() {
		logger.debug("destory httpClient start...");
		if(null != httpClient) {
			try {
				httpClient.close();
			} catch (IOException e) {
				logger.error("destory httpClient exception.", e);
			}
		}
		logger.debug("destory httpClient end.");
	}
	
    static {
    	//配置初始化
    	init();
    	
        PoolingHttpClientConnectionManager cm = getPoolingHttpClientConnectionManager();
        ConnectionKeepAliveStrategy myStrategy = getConnectionKeepAliveStrategy();
        HttpRequestRetryHandler myRetryHandler = getHttpRequestRetryHandler();
        HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(cm)// 设置连接池
                .setKeepAliveStrategy(myStrategy)// 设置连接存活策略
                .setRetryHandler(myRetryHandler);// 设置请求重试;

        if (StringUtils.isNotEmpty(proxyUri) && proxyPort != 0) {
            HttpHost httpHost = new HttpHost(proxyUri, proxyPort, proxyScheme);
            httpClientBuilder.setProxy(httpHost);
            if (StringUtils.isNotEmpty(proxyUserName) && StringUtils.isNotEmpty(proxyPassword)) {
                CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
                credentialsProvider.setCredentials(new AuthScope(httpHost),
                        new UsernamePasswordCredentials(proxyUserName, proxyPassword));
                httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
            }
        }
        httpClient = httpClientBuilder.build();
        idleConnectionMonitorThread = new IdleConnectionMonitorThread(cm, connectRecyclingTime, connectFreeTimeout);// 设置连接回收策略
        idleConnectionMonitorThread.start();
    }
    
    /**
     * 配置加载
     * @author xiongxiaotun <br/>
     * @Title init  <br/>
     * @Description 配置加载 <br/>
     * @version V1.0  <br/>
     */
    private static void init(){
    	try {
			Properties props = new Properties();
			props.load(HttpClient.class.getResourceAsStream("http.properties"));
			String connect_max_total  = (String)props.get("httpclient.connect_max_total");
			connectMaxTotal = StringUtils.isBlank(connect_max_total) ? 200 : Integer.parseInt(connect_max_total);
			
			String connect_max_default  = (String)props.get("httpclient.connect_max_default");
			connectMaxDefault = StringUtils.isBlank(connect_max_default) ? 20 : Integer.parseInt(connect_max_default);
			
			String connect_recycling_time  = (String)props.get("httpclient.connect_recycling_time");
			connectRecyclingTime = StringUtils.isBlank(connect_recycling_time) ? 5 : Integer.parseInt(connect_recycling_time);
			
			String connect_free_timeout  = (String)props.get("httpclient.connect_free_timeout");
			connectFreeTimeout = StringUtils.isBlank(connect_free_timeout) ? 120 : Integer.parseInt(connect_free_timeout);
			
			String connect_keep_alive_duration  = (String)props.get("httpclient.connect_keep_alive_duration");
			connectKeepAliveDuration = StringUtils.isBlank(connect_keep_alive_duration) ? 30 : Integer.parseInt(connect_keep_alive_duration);
			
			String connect_retry_number  = (String)props.get("httpclient.connect_retry_number");
			connectRetryNumber = StringUtils.isBlank(connect_retry_number) ? 2 : Integer.parseInt(connect_retry_number);
			
			String connect_timeout  = (String)props.get("httpclient.connect_timeout");
			connectTimeout = StringUtils.isBlank(connect_timeout) ? 10000 : Integer.parseInt(connect_timeout);
			
			String connect_request_timeout  = (String)props.get("httpclient.connect_request_timeout");
			connectRequestTimeout = StringUtils.isBlank(connect_request_timeout) ? 15000 : Integer.parseInt(connect_request_timeout);
			
			String socket_timeout  = (String)props.get("httpclient.socket_timeout");
			socketTimeout = StringUtils.isBlank(socket_timeout) ? 10000 : Integer.parseInt(socket_timeout);
		} catch (Exception e) {
			logger.debug("加载客户端配置.异常栈:{}", e);
		}
    }
}
